Arduino Sketch
The GPS Tracker demo utilizes the Adafruit GPS library to communicate with the GPS. In order to install it, head to the Arduino IDE
-> Tools
-> Manage Libraries
and search for Adafruit GPS
.
You should also download the AVR-IoT MCP9808
and AVR-IoT VEML3328
drivers for the AVR-IoT Cellular board (also found in the library manager in the Arduino IDE). These libraries are for the light sensor and the temperature sensor on the board. In order to save as much power as possible when running on battery, these libraries are needed so that we can power these sensors down.
Code
You can find the whole Arduino Sketch in the examples for the AVR-IoT Cellular Library. In the Arduino IDE, navigate to File
-> Examples
-> AVR-IoT Cellular
-> gps_tracker
.
Setup
In order to retrieve messages from the GPS, we have to initialize it. This can be done with the following snippet. In this snippet, the serial line to the GPS is set up, and commands are sent to set the output messages wanted, the refresh rate of the GPS and to disable antenna updates.
#include <Arduino.h>
#include <Adafruit_GPS.h>
// ...
#define GPSSerial Serial2
/**
* @brief Interface for the GPS.
*/
Adafruit_GPS GPS(&GPSSerial);
/**
* @brief Starts the GPS modem and sets up the configuration.
*/
static void initializeGPS(void) {
GPSSerial.swap(1);
GPS.begin(9600);
// Enable RMC & GGA output messages
GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
// Set the update rate to 1Hz
GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);
// Disable updates on antenna status
GPS.sendCommand(PGCMD_NOANTENNA);
delay(1000);
// Ask for firmware version
GPSSerial.println(PMTK_Q_RELEASE);
}
HTTP is used to send the GPS position to the web server and has to be initialized. In this example, we utilize HTTP and not HTTPS to keep things simple. The port we utilize is 8080. Don't worry about the domain name for now, we'll set that up later.
#include <http_client.h>
// ...
// Not needed to be filled out for the moment, will be elaborated on later in the tutorial
#define HTTP_DOMAIN "<your server IP here>"
/**
* @brief Sets up the HTTP client with the given domain
*/
static void initializeHTTP(void) {
if (!HttpClient.configure(HTTP_DOMAIN, 8080, false)) {
Log.info("Failed to configure HTTP client");
} else {
Log.info("Configured HTTP");
}
}
Now we can finalize the setup function. Note that the Led Controller is being initialized as well, such that the LEDs for cellular network connectivity, GPS fix, and sending data can be utilized.
In the setup function, we also configure the modem for low power and shut down the temperature and light sensor to save power.
#include <led_ctrl.h>
#include <log.h>
#include <low_power.h>
#include <lte.h>
#include <mcp9808.h>
#include <veml3328.h>
// ...
void setup() {
LedCtrl.begin();
LedCtrl.startupCycle();
Log.begin(115200);
Log.info("Starting AVR-IoT Cellular Adafruit GPS example");
// We configure the low power module for power down configuration, where
// the modem and the CPU will be powered down
LowPower.configurePowerDown();
// Make sure sensors are turned off
Veml3328.begin();
Mcp9808.begin();
Veml3328.shutdown();
Mcp9808.shutdown();
initializeGPS();
initializeHTTP();
}
Sending data
In order to send the data, we first need to parse the GPS information. The latitude, longitude, and timestamp of the GPS position are stored in strings which are later sent with HTTP.
In this snippet, we first check if there are any incoming data and read it if that is the case. This is important as the GPS library updates its state when a read is issued.
After that, we check if a GPS message has been received (an NMEA message) and parse it. If the GPS has fix (has got a valid position), we convert the latitude and longitude float values to strings with the help of dtostrf
. We'll come back to the has_parsed
variable later for the main loop.
static char latitude[16] = "0";
static char longitude[16] = "0";
static char time[24] = "0";
/**
* @brief Whether or not we've parsed one GPS entry. Prevents sending zeros
* whilst having fix after boot.
*/
static bool has_parsed = false;
/**
* @brief Checks for new GPS messages and parses the NMEA messages if any.
*/
static void parseGPSMessages() {
// Read the incoming messages, needn't do anything with them yet as that is
// taken care of by the newNMEAReceived() function.
if (GPS.available()) {
GPS.read();
}
if (GPS.newNMEAreceived()) {
if (!GPS.parse(GPS.lastNMEA())) {
// If we fail to parse the NMEA, wait for the next one
return;
} else {
Log.rawf("Timestamp: %d/%d/20%d %d:%d:%d GMT+0 \r\n",
GPS.day,
GPS.month,
GPS.year,
GPS.hour,
GPS.minute,
GPS.seconds);
Log.rawf("Fix: %d, quality: %d\r\n", GPS.fix, GPS.fixquality);
if (GPS.fix) {
// Need to convert all floats to strings
dtostrf(GPS.latitudeDegrees, 2, 4, latitude);
dtostrf(GPS.longitudeDegrees, 2, 4, longitude);
sprintf(time,
"%d/%d/20%d %d:%d:%d",
GPS.day,
GPS.month,
GPS.year,
GPS.hour,
GPS.minute,
GPS.seconds);
Log.rawf("Location: %s N, %s E\r\n", latitude, longitude);
Log.rawf("Satellites: %d\r\n", GPS.satellites);
Log.rawf("\r\n");
has_parsed = true;
}
}
}
}
Now that we got the GPS message parsing done, we can write the function for sending the data. This function will send a HTTP POST
message with the latest latitude, longitude and timestamp to the webserver on the /data
endpoint.
/**
* @brief Sends a payload with latitude, longitude and the timestamp.
*/
static void sendData(void) {
char data[80] = "";
sprintf(data,
"{\"lat\":\"%s\",\"lon\":\"%s\",\"time\": \"%s\"}",
latitude,
longitude,
time);
HttpResponse response = HttpClient.post("/data", data);
Log.infof("POST - status code: %u, data size: %u\r\n",
response.status_code,
response.data_size);
if (response.status_code != 0) {
String body = HttpClient.readBody(512);
if (body != "") {
Log.infof("Response: %s\r\n", body.c_str());
}
}
}
Main loop
We utilize an enum to keep track of the state of the application. We define three states: not connected to the cellular network, connected to the cellular network and connected with GPS fix (valid position).
We also need a function to connect to the LTE network, which will be utilized in the start and also when reception is lost in order to reconnect.
/**
* @brief Connected refers to the LTE network.
*/
enum class State { NOT_CONNECTED, CONNECTED, CONNECTED_WITH_FIX };
/**
* @brief Keeps track of the state.
*/
static State state = State::NOT_CONNECTED;
/**
* @brief Connects to the network operator. Will block until connection is
* achieved.
*/
static void connectToNetwork() {
// If we already are connected, don't do anything
if (!Lte.isConnected()) {
while (!Lte.begin()) {}
Log.infof("Connected to operator: %s\r\n", Lte.getOperator().c_str());
}
state = State::CONNECTED;
}
The only thing left then is the main loop. In the loop we first parse the GPS messages.
After that, we check which state we're in. If we are not connected to the cellular network, we connect to it. If we are connected but don't have GPS fix yet, we wait until we get fix. When we get fix, we decrease the update rate of the GPS in order to save power.
When the device has a cellular connection and GPS fix, we send the location data before we set the GPS in standby mode and power down the modem as well as the MCU for 60 seconds. When the device wakes up, it will automatically connect to the cellular network, so that is not needed to do manually.
The has_parsed
variable is utilized here so that the application has to retrieve a new GPS position after each wake up (so that it doesn't just immediately send the last known position and goes back to sleep).
void loop() {
parseGPSMessages();
switch (state) {
case State::NOT_CONNECTED:
connectToNetwork();
break;
case State::CONNECTED:
if (!Lte.isConnected()) {
state = State::NOT_CONNECTED;
} else if (GPS.fix) {
state = State::CONNECTED_WITH_FIX;
LedCtrl.on(Led::CON);
// Decrease update rate once we have fix to save power
GPS.sendCommand(PMTK_SET_NMEA_UPDATE_100_MILLIHERTZ);
}
break;
case State::CONNECTED_WITH_FIX:
if (!Lte.isConnected()) {
state = State::NOT_CONNECTED;
} else if (!GPS.fix) {
// Lost fix, set update rate back to 1 Hz
GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);
state = State::CONNECTED;
LedCtrl.off(Led::CON);
}
if (Lte.isConnected() && GPS.fix && has_parsed) {
sendData();
// Reset state before we power down, which will turn of the modem
state = State::NOT_CONNECTED;
LedCtrl.off(Led::CON);
Log.info("Entering low power");
GPS.sendCommand(PMTK_STANDBY);
delay(1000); // Allow some time to print messages before we sleep
LowPower.powerDown(60);
Log.info("Woke up!");
GPS.sendCommand(PMTK_AWAKE);
// Set that we need an update for the position
has_parsed = false;
}
break;
}
}
Power consumption
For this demo application, the device will consume approximately 18.5 mA on average when waking up and sending the position every minute. This can be reduced however if the device is kept powered down longer. This is dependent on the application. If you only need to send the position every hour or every day, this number will be drastically reduced.
When utilizing an average of 18.5 mA, this will result in approximately 65 hours or 2 days and 17 hours of battery life with a 1200mAh battery. As mentioned, this number can be drastically improved if one keeps the device powered down for longer.